package seasonEvent

import (
	"fmt"
	"gitee.com/kingzyt/common/dbMysql"
	"gitee.com/kingzyt/common/dbRedis"
	"gitee.com/kingzyt/common/log"
	. "gitee.com/kingzyt/common/util"
	"strings"
	"sync"
	"time"
)

var NewDefaultStaticFixedSeasonBaseDBOperator = func(name string, dbKey string) IStaticFixedSeasonBaseDBOperator {
	return interface{}(NewStaticFixedSeasonBaseMysqlOperator(name, dbKey)).(IStaticFixedSeasonBaseDBOperator)
	//return interface{}(NewStaticFixedSeasonBaseRedisOperator(name, dbKey)).(IStaticFixedSeasonBaseDBOperator)
}

///////////////////////////////////////////////////////
type IStaticFixedSeasonBaseDBOperator interface {
	SetDBAccessor(a interface{}) bool
	GetData() (nextCPT int64, seasonVer int64, ok bool)
	SetData(nextCPT int64, seasonVer int64) (ok bool)
}

type StaticFixedSeasonBaseRedisOperator struct {
	name             string
	dbKey            string
	dbac             *dbRedis.DBAccessor
	db_season_load   *dbRedis.DBScript
	db_season_switch *dbRedis.DBScript
	signal           chan byte
}

func NewStaticFixedSeasonBaseRedisOperator(name string, dbKey string) *StaticFixedSeasonBaseRedisOperator {
	return &StaticFixedSeasonBaseRedisOperator{
		name:   name,
		dbKey:  dbKey,
		signal: make(chan byte, 1),
	}
}

func (self *StaticFixedSeasonBaseRedisOperator) SetDBAccessor(a interface{}) bool {
	self.dbac = a.(*dbRedis.DBAccessor)

	self.db_season_load = dbRedis.NewDBScript("db_"+self.dbKey+"_season_load", 0, 1, strings.Join([]string{`
local nextCPT = 0
local seasonVer = 0
local seasonDatas = redis.call("HMGET", "`, self.dbKey, `:season", "nextCPT", "seasonVer")
if seasonDatas[1] ~= false then
	nextCPT = tonumber(seasonDatas[1])
end
if seasonDatas[2] ~= false then
	seasonVer = tonumber(seasonDatas[2])
end

return {1, nextCPT, seasonVer}
`}, ""))

	self.db_season_switch = dbRedis.NewDBScript("db_"+self.dbKey+"_season_switch", 0, 16, strings.Join([]string{`
local nextCPT = ARGV[1]
local seasonVer = ARGV[2]
redis.call("HMSET", "`, self.dbKey, `:season", "nextCPT", nextCPT, "seasonVer", seasonVer)
return {1}
`}, ""))

	self.dbac.AddDBScript(self.db_season_load)
	self.dbac.AddDBScript(self.db_season_switch)
	return true
}
func (self *StaticFixedSeasonBaseRedisOperator) GetData() (nextCPT int64, seasonVer int64, ok bool) {
	ok = self.dbac.SyncOperateDB(self.name, self.db_season_load, self.signal, func(datas []interface{}) {
		nextCPT = dbRedis.GetInt64P(datas[0], "nextCPT")
		seasonVer = dbRedis.GetInt64P(datas[1], "nextCseasonVerPT")
	})
	return
}
func (self *StaticFixedSeasonBaseRedisOperator) SetData(nextCPT int64, seasonVer int64) (ok bool) {
	return self.dbac.SyncOperateDB(self.name, self.db_season_switch, self.signal, nil, nextCPT, seasonVer)
}

type StaticFixedSeasonBaseMysqlOperator struct {
	name  string
	dbKey string
	sqlrw *dbMysql.SimpleMySqlRWer
}

func NewStaticFixedSeasonBaseMysqlOperator(name string, dbKey string) *StaticFixedSeasonBaseMysqlOperator {
	return &StaticFixedSeasonBaseMysqlOperator{
		name:  name,
		dbKey: dbKey,
	}
}

func (self *StaticFixedSeasonBaseMysqlOperator) SetDBAccessor(a interface{}) bool {
	self.sqlrw = a.(*dbMysql.SimpleMySqlRWer)
	_, err := self.sqlrw.WriteQuery(
		`create table if not exists SeasonSwitch
(
	DbKey varchar(256) not null primary key,
	NextCPT bigint not null,
	SeasonVer bigint not null
)
engine = innodb
default character set = utf8mb4 collate = utf8mb4_bin;
`)
	if err != nil {
		log.Error(self.name, "SetDBAccessor err: %s", err)
	}
	return err == nil
}
func (self *StaticFixedSeasonBaseMysqlOperator) GetData() (nextCPT int64, seasonVer int64, ok bool) {
	err := self.sqlrw.ReadQueryRow("select NextCPT,SeasonVer from SeasonSwitch where DbKey=?", []interface{}{&nextCPT, &seasonVer}, nil, self.dbKey)
	if err != nil {
		log.Error(self.name, "GetData, err: %s", err)
	}
	ok = err == nil
	return
}
func (self *StaticFixedSeasonBaseMysqlOperator) SetData(nextCPT int64, seasonVer int64) (ok bool) {
	_, err := self.sqlrw.WriteQuery("insert into SeasonSwitch(DbKey,NextCPT,SeasonVer) values(?,?,?) on duplicate key update NextCPT=?,SeasonVer=?", self.dbKey, nextCPT, seasonVer, nextCPT, seasonVer)
	if err != nil {
		log.Error(self.name, "SetData, err: %s", err)
	}
	ok = err == nil
	return
}

//////////////////////////////////////////////////

type GetCheckpointTFunc func(nextCPT int64, now time.Time) (newCPT int64, changed bool)

type StaticFixedSeasonBase struct {
	name string
	dbop IStaticFixedSeasonBaseDBOperator

	infoOnSeasonSwitch string
	getCheckpointT     GetCheckpointTFunc

	mutex sync.Mutex

	/*
		[可能在调试过程中出现的困惑]:
		在修改时间后，发现seasonVer没有切换？
		因为之前有一次测试将时间设置为了更加后面, 并且触发了seasonVer切换, nextCPT修改了, 然后又在后面的测试中将时间重置为正常时间,
		而且之后的测试中忘记了有这么回事, 于是即使加了一两天还是比nextCPT要早，触发不了切日期...
	*/
	nextCPT   AtomicInt64
	seasonVer AtomicInt64

	OnSeasonSwitch func(nextCPT int64, curVer int64)
}

func NewStaticFixedSeasonBase(name string, dbop IStaticFixedSeasonBaseDBOperator, infoOnSeasonSwitch string, getCheckpointT GetCheckpointTFunc) *StaticFixedSeasonBase {
	return &StaticFixedSeasonBase{
		name: name,
		dbop: dbop,

		infoOnSeasonSwitch: infoOnSeasonSwitch,
		getCheckpointT:     getCheckpointT,
	}
}

func (self *StaticFixedSeasonBase) SetDBAccessor(a interface{}) bool {
	return self.dbop.SetDBAccessor(a)
}

// 因为这里的dbop是sync的, 所以不要在外部嵌套其他的lock, 以防止预期外的死锁
func (self *StaticFixedSeasonBase) Start() bool {
	self.mutex.Lock()
	defer self.mutex.Unlock()
	nextCPT, seasonVer, ok := self.dbop.GetData()
	if ok {
		self.nextCPT.Set(nextCPT)
		self.seasonVer.Set(seasonVer)
	}
	return ok
}

// 因为这里的dbop是sync的, 所以不要在外部嵌套其他的lock, 以防止预期外的死锁
func (self *StaticFixedSeasonBase) CheckSeasonSwitch(now time.Time) {
	defer DeferRecoverPanic(self.name)

	_, changed := self.getCheckpointT(self.nextCPT.Get(), now)
	if changed {
		self.mutex.Lock()
		defer self.mutex.Unlock()

		nextCPT, changed := self.getCheckpointT(self.nextCPT.Get(), now)
		if changed {
			ok := self.dbop.SetData(nextCPT, self.seasonVer.Get()+1)
			if ok {
				self.nextCPT.Set(nextCPT)
				self.seasonVer.Add(1)
				if self.OnSeasonSwitch != nil {
					self.OnSeasonSwitch(self.nextCPT.Get(), self.seasonVer.Get())
				}
				log.Info(self.name, log.I, "%s OK, seasonVer: %d", self.infoOnSeasonSwitch, self.seasonVer.Get())
			}
		}
	}
}

func (self *StaticFixedSeasonBase) GetSeasonVer() int64 {
	return self.seasonVer.Get()
}
func (self *StaticFixedSeasonBase) GetNextCPT() int64 {
	return self.nextCPT.Get()
}

//////////////////////////////////////////////////////////
type StaticFixedDaySeason struct {
	*StaticFixedSeasonBase
	verToDate      map[int64]int32
	verToDateMutex sync.RWMutex
}

func NewStaticFixedDaySeason(name string, dbKey string) *StaticFixedDaySeason {
	rlt := &StaticFixedDaySeason{
		StaticFixedSeasonBase: NewStaticFixedSeasonBase(name, NewDefaultStaticFixedSeasonBaseDBOperator(name, dbKey),
			"switch to new day season",
			GetCheckpointT),
		verToDate: map[int64]int32{},
	}
	rlt.StaticFixedSeasonBase.OnSeasonSwitch = rlt.onSeasonSwitch_

	return rlt
}

func (self *StaticFixedDaySeason) CheckSeasonSwitch(now time.Time) {
	self.StaticFixedSeasonBase.CheckSeasonSwitch(now)

	self.verToDateMutex.Lock()
	defer self.verToDateMutex.Unlock()
	_, ok := self.verToDate[self.seasonVer.Get()]
	if !ok && time.Now().Hour() >= New_day_checkpoint_hour {
		self.nowToYMD(self.seasonVer.Get())
	}
}

func (self *StaticFixedDaySeason) nowToYMD(ver int64) {
	y, m, d := time.Now().Date()
	self.verToDate[ver] = int32(y*10000 + int(m)*100 + d)
}

func (self *StaticFixedDaySeason) onSeasonSwitch_(nextCPT int64, ver int64) {
	self.verToDateMutex.Lock()
	defer self.verToDateMutex.Unlock()
	self.nowToYMD(ver)
}

// only for user online duration, if server start between [0,4)clock, user will lost online duration time length in this range
func (self *StaticFixedDaySeason) GetYMD(ver int64) int32 {
	self.verToDateMutex.RLock()
	defer self.verToDateMutex.RUnlock()

	ymd, ok := self.verToDate[ver]
	if !ok {
		log.Error(self.name, "GetYMD, ver:%d, is not defined", ver)
	}
	return ymd
}

//////////////////
type StaticFixedWeekSeason struct {
	*StaticFixedSeasonBase
}

func NewStaticFixedWeekSeason(name string, dbKey string, weekDayForSwitch time.Weekday) *StaticFixedWeekSeason {
	return &StaticFixedWeekSeason{
		StaticFixedSeasonBase: NewStaticFixedSeasonBase(name, NewDefaultStaticFixedSeasonBaseDBOperator(name, dbKey),
			"switch to new week season",
			func(nextCPT int64, now time.Time) (newCPT int64, changed bool) {
				newCPT, changed = GetCheckpointTByWeek(nextCPT, now, weekDayForSwitch)
				return
			}),
	}
}

//////////////////
type StaticFixedHourPointSeason struct {
	*StaticFixedSeasonBase
}

func NewStaticFixedHourPointSeason(name string, dbKey string, hourPoints []int32) *StaticFixedHourPointSeason {
	if len(hourPoints) <= 0 {
		panic("NewStaticFixedHourPointSeason: hourPoints len should be > 0")
	}
	for i := 0; i < len(hourPoints); i++ {
		if hourPoints[i] < 0 || hourPoints[i] > 23 {
			panic(fmt.Sprintf("NewStaticFixedHourPointSeason: hourPoints[%d](%d) is out of range [0,23]", i, hourPoints[i]))
		}
	}

	return &StaticFixedHourPointSeason{
		StaticFixedSeasonBase: NewStaticFixedSeasonBase(name, NewDefaultStaticFixedSeasonBaseDBOperator(name, dbKey),
			"switch to new hourPoint season",
			func(nextCPT int64, now time.Time) (newCPT int64, changed bool) {
				newCPT, changed = GetCheckpointTByCplist(nextCPT, now, hourPoints)
				return
			}),
	}
}

///////////////////
type StaticFixedIntervalSeason struct {
	*StaticFixedSeasonBase
}

func NewStaticFixedIntervalSeason(name string, dbKey string, interval time.Duration) *StaticFixedIntervalSeason {
	return &StaticFixedIntervalSeason{
		StaticFixedSeasonBase: NewStaticFixedSeasonBase(name, NewDefaultStaticFixedSeasonBaseDBOperator(name, dbKey),
			"switch to new interval season",
			func(nextCPT int64, now time.Time) (newCPT int64, changed bool) {
				nowT := now.UnixNano()
				if nextCPT == 0 {
					return nowT + int64(interval), true
				}
				if nextCPT < nowT {
					cnt := (nowT - nextCPT) / int64(interval)
					cnt++
					return nextCPT + cnt*int64(interval), true
				}
				return nextCPT, false
			}),
	}
}
